import fs from 'fs'
import JSON5 from 'json5'
import path from 'path'

/**
 * 判断是否是数字
 * @param {*} str
 * @returns {boolean}
 */
function isNumber(str) {
  return !isNaN(parseFloat(str)) && isFinite(str);
}

/**
 * 判断是否是数字开头
 * @param {string} str
 * @returns {boolean}
 */
function isStartsWithNumber(str) {
    return /^\d/.test(str);
}

/**
 * 首字母大写
 * @param {string} txt
 * @returns {string}
 */
function capitalizeKey(key) {
  return key.charAt(0).toUpperCase() + key.slice(1)
}

/**
 * 格式化类名
 * @param {string} originName
 * @returns {string}
 */
function formatClassName(originName) {
  const names = originName.split(/[-_]/).filter((item) => item != '')
  let name = ''
  for (let i = 0; i < names.length; i++) {
    name += capitalizeKey(names[i])
  }
  return name
}

/**
 * 格式化属性键值
 * @param {string} originKey
 * @returns {string}
 */
function formatPropertyKey(originKey) {
  const keys = originKey.split(/[-_]/).filter((item) => item != '')
  let key = keys[0]
  for (let i = 1; i < keys.length; i++) {
    key += capitalizeKey(keys[i])
  }
  return key
}

class PropertyNode {
  /**
   * @param {string} key
   * @param {string} type [string, number, boolean, object, stringArray, numberArray, booleanArray, objectArray]
   */
  constructor(key, type) {
    /** @tyle {string} */
    this.key = key
    /** @tyle {string} */
    this.type = type
    /** @type {string} */
    this.newKey = formatPropertyKey(key)
  }

  toPropertyString() {
    switch (this.type) {
      case 'string':
        return `/** @type {string} */\nthis.${this.newKey} = json['${this.key}'] || ''`
      case 'number':
        return `/** @type {number} */\nthis.${this.newKey} = json['${this.key}'] || 0`
      case 'boolean':
        return `/** @type {boolean} */\nthis.${this.newKey} = json['${this.key}'] || false`
      case 'object':
        const objClz = formatClassName(`${this.newKey}DTO`)
        return `/** @type {${objClz}} */\nthis.${this.newKey} = ${objClz}.fromJson(json['${this.key}'] || {})`
      case 'stringArray':
        return `/** @type {string[]} */\nthis.${this.newKey} = json['${this.key}'] || []`
      case 'numberArray':
        return `/** @type {number[]} */\nthis.${this.newKey} = json['${this.key}'] || []`
      case 'booleanArray':
        return `/** @type {boolean[]} */\nthis.${this.newKey} = json['${this.key}'] || []`
      case 'objectArray':
        const objArrClz = formatClassName(`${this.newKey}DTO`)
        return `/** @type {${objArrClz}[]} */\nthis.${this.newKey} = (json['${this.key}'] || []).map((item) => ${objArrClz}.fromJson(item))`
    }
    return ''
  }

  toKeyValueString() {
    switch (this.type) {
      case 'object':
        return `obj['${this.key}'] = JSON.parse(this.${this.newKey}.toJson())`
      case 'objectArray':
        return `obj['${this.key}'] = this.${this.newKey}.map((item) => JSON.parse(item.toJson()))`
      default:
        return `obj['${this.key}'] = this.${this.newKey}`
    }
  }
}

class EntityClass {
  /**
   * @param {string} clzName
   * @param {PropertyNode[]} nodes
   */
  constructor(clzName, nodes) {
    /** @type {string} */
    this.clzName = clzName
    /** @type {PropertyNode[]} */
    this.nodes = nodes
  }

  toEntityClassString() {
    return `/**
 * ${this.clzName}
 */
${this.clzName.endsWith('Entity') ? 'export ' : ''}class ${this.clzName} {
  /**
   * @param {object} json
   */
  constructor(json) {
${this.nodes.map((node) => `    ${node.toPropertyString().replace('\n', '\n    ')}`).join('\n')}
  }

  /**
   * @returns {string}
   */
  toJson() {
    const obj = {}
${this.nodes.map((node) => `    ${node.toKeyValueString()}`).join('\n')}
    return JSON.stringify(obj)
  }

  /**
   * @param {object} json
   * @return {${this.clzName}}
   */
  static fromJson(json) {
    return new ${this.clzName}(json)
  }
}`
  }
}

/**
 * 转化JSON为JavaScript类
 * @param {string} jsonFile   JSON文件路径
 * @param {string} filename   类文件名 
 * @param {string} outputPath 输出目录 
 */
function json2jsclass(jsonFile, filename, outputPath) {
  /** @type {EntityClass[]} */
  const entities = []
  /**
   * 创建实体类
   * @param {string} name
   * @param {object} json
   * @param {boolean} isEntity
   */
  function createEntityNode(name, json, isEntity) {
    /** @type {PropertyNode[]} */
    const nodes = []
    const keys = Object.keys(json)
    for (const key of keys) {
      const value = json[key]
      const type = typeof value
      switch (type) {
        case 'string':
        case 'number':
        case 'boolean':
          nodes.push(new PropertyNode(key, type))
          break
        case 'object':
          const isObject = value.length === undefined
          if (isObject) {
            createEntityNode(key, value, false)
            nodes.push(new PropertyNode(key, type))
          } else {
            const subValue = value[0]
            const subType = typeof subValue
            switch (subType) {
              case 'string':
              case 'number':
              case 'boolean':
                nodes.push(new PropertyNode(key, `${subType}Array`))
                break
              case 'object':
                createEntityNode(key, subValue, false)
                nodes.push(new PropertyNode(key, 'objectArray'))
                break
            }
          }
          break
      }
    }
    name = formatClassName(name)
    const clzName = isEntity ? `${name}` : `${name}DTO`
    entities.push(new EntityClass(clzName, nodes))
  }
  const jsonObj = JSON5.parse(fs.readFileSync(jsonFile, 'utf8'))
  console.log(jsonObj)
  createEntityNode(filename, jsonObj, true)

  const jsonString = entities
    .reverse()
    .map((entity) => entity.toEntityClassString())
    .join('\n\n')
  // console.log(jsonString)
  const writeData = `//////////////////////////////////////////////////////////////////////////////////
//                                                                              //
//                    GENERATED CODE - DO NOT MODIFY BY HAND                    //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////\n\n${jsonString}\n`
  fs.writeFileSync(outputPath, writeData, 'utf8')
  console.log(`Converted JSON to JavaScript class: ${outputPath}`)
}

export function convertJsonToClass(jsonFile, outputDir) {
  console.log('Converting JSON to class...')
  console.log(`jsonFile: ${jsonFile}`);
  console.log(`outputDir: ${outputDir}`);

  let filename = path.basename(jsonFile, jsonFile.endsWith('.json5') ? '.json5' : '.json')
  filename = filename.toLowerCase().replace(/[-_.]/g, ' ').trim().replace(/[ ]+?/g, '-')

  if (filename.length === 0) {
    filename = 'base-entity'
  }
  if(filename.endsWith('-entity') === false) {
    filename += '-entity'
  }
  if(isStartsWithNumber(filename)) {
    filename = '$' + filename
  }

  console.log(`filename: ${filename}`);

  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  const outputPath = path.join(outputDir, filename + '.js');
  console.log(`outputPath: ${outputPath}`);

  json2jsclass(jsonFile, filename, outputPath)
}